Skip to content

JVM GC 总结

Java 虚拟机(JVM)通过垃圾回收(GC)自动管理堆内存。理解 GC,需要从两个层面入手:

  1. GC 的机制与概念:Minor GC、Full GC、Stop-the-World、对象引用关系;
  2. GC 算法与收集器:ParNew、CMS、G1 等具体收集器是如何实现回收的。

本文先讲清前者,再深入后者。


GC 的基础概念

1.1 Minor GC(年轻代回收)

"Minor GC" 主要针对年轻代(Young Generation)进行垃圾回收。年轻代通常包含新创建的对象,而且大多数对象都会很快变得不可达(即不再被引用)。"Minor GC" 的主要目标是尽早回收这些短时间存活的对象,以防止它们占据年轻代内存空间。

过程:

  1. 标记存活对象:扫描年轻代,标记仍被引用的对象(基于复制算法)。
  2. 清理无用对象:未被引用的对象直接释放。
  3. 晋升老年代:经历多次 Minor GC 仍存活的对象,会被晋升到老年代。

Minor GC 触发频繁,但单次停顿很短。

1.2 Full GC(老年代回收)

"Full GC" 主要针对老年代进行垃圾回收。老年代通常包含对象的生命周期较长、存活时间较长的对象。"Full GC" 的主要目标是回收老年代中不再被引用的对象,以释放内存空间。

过程:

  1. 标记存活对象:扫描整个堆(年轻代 + 老年代)。
  2. 清理无用对象:释放所有不可达对象。
  3. 压缩内存:通常还会进行内存整理,减少碎片。

Full GC 触发不频繁,但单次停顿时间较长,是性能调优重点关注的对象。

1.3 Stop-the-World

无论 Minor GC 还是 Full GC,都可能引发 Stop-the-World(STW) 暂停——所有应用线程被挂起,直到 GC 完成。

GC 类型STW 时长原因
Minor GC较短只回收年轻代,存活对象少,复制快
Full GC较长扫描整个堆,且通常包含整理/压缩

在 Java 中,GC 暂停的目的是为了确保垃圾回收器能够安全地标记和清理不再使用的对象,同时防止并发执行的线程与正在进行的垃圾回收操作发生冲突。这些 "Stop-the-World" 暂停事件对于垃圾回收是必要的,但对于某些应用程序可能会导致性能问题,因为在暂停期间,应用程序不能继续执行任何工作。

现代 JVM 通过分代、并行、并发等技术尽量缩短 STW,但无法完全消除。

1.4 Outgoing Reference(出站引用)

GC 的本质是追踪对象之间的引用关系,判断哪些对象不可达,从而回收。这里有两个相对的概念:

  • Outgoing Reference(出站引用) :对象 A 中有字段指向对象 B,则 A 对 B 有一个出站引用。
  • Incoming Reference(入站引用) :反过来,B 被 A 引用,则 B 有一个来自 A 的入站引用。

GC 正是通过遍历出站引用链(从 GC Roots 出发)来识别可达对象。需要特别注意循环引用——A 引用 B,B 又引用 A——如果它们整体与 GC Roots 不可达,现代 GC 仍能正确回收;但若在引用链上意外保留,则会导致内存泄漏


HotSpot 的分代收集器组合

HotSpot 的准确二分法:

  • Partial GC: 只清理堆的一部分
  • Full GC: 清理整个堆(Young + Old,通常还带 Metaspace)

在 Partial GC 下面又可以细分:

  • Young GC / Minor GC → 只清新生代
  • Old GC只清老年代CMS 的并发收集就是这个,只有 CMS 能做到单独收老年代而不碰新生代
  • Mixed GC → 新生代 + 部分老年代(G1 专属)

HotSpot 的 Partial GC 采用分代收集器组合(collector pair) :年轻代和老年代由不同的收集器分工协作。

经典组合是 ParNew + CMS

-XX:+UseConcMarkSweepGC

Young: ParNew      (Minor GC)
Old:   CMS         (Major / Old GC)
  • CMS 不处理年轻代
  • 必须有一个 Young GC
  • ParNew 是 唯一能与 CMS 正常配合的并行年轻代收集器
Java Heap
 ├── Young Generation
 │     └── ParNew  (Minor GC)

 └── Old Generation
       └── CMS     (Major / Old GC)

之所以是经典低延迟组合,原因有二:

  1. 分工明确
    • ParNew 像高效的街道清洁工,频繁、快速地清扫年轻代的"落叶"(短命对象),停顿短到可以接受。
    • CMS 像深夜施工队,处理老年代的"建筑垃圾",并发执行,只在两次短暂 STW 时短暂封路。
  2. 目标一致:都在减少应用感知到的停顿——ParNew 靠"快",CMS 靠"并发"。

注:CMS 不处理年轻代,必须搭配一个 Young GC 收集器,而 ParNew 是唯一能与 CMS 正常配合的并行年轻代收集器


新生代收集器:ParNew

ParNew 是 Serial 收集器的多线程并行版本,专为配合 CMS 等老年代收集器而设计。

3.1 核心思想

  • 目标:高吞吐、高效率地回收新生代。
  • 算法:停止-复制。基于假设"98% 以上的对象很快死亡",因此不去标记大量死对象,而是只关注少数存活对象,把它们复制到安全区域

3.2 工作流程(一次 Minor GC,全程 STW)

parnew_gc_notebook

  1. GC Roots 追踪(多线程并行) 暂停所有应用线程,多线程并行扫描 GC Roots(栈帧局部变量、静态变量等),找到第一批存活对象。
  2. 对象图标记(多线程并行) 从 GC Roots 出发,并行遍历新生代引用图,标记所有存活对象。
  3. 复制与晋升
    • 计算存活对象目标位置(To Survivor 或老年代)。
    • 将 Eden 和 From Survivor 的存活对象紧凑复制到 To Survivor,自然消除碎片。
    • 每存活一次 Minor GC,对象年龄 +1;年龄超过阈值(默认 15)或 To Survivor 空间不足时,对象晋升到老年代。
  4. 空间清理 Eden 与 From Survivor 直接整块清空,From/To 角色交换。

3.3 特点与适用场景

  • 优点:多线程并行,速度快;复制算法天然无碎片。
  • 缺点:仍会 STW,但因新生代小、存活对象少,停顿通常只有几十到一两百毫秒。
  • 场景:追求响应速度的应用的标配,承担绝大部分日常 GC 工作。

老年代收集器:CMS

CMS(Concurrent Mark-Sweep) 是一款以最短停顿时间为目标的老年代收集器。

4.1 核心思想

  • 目标:最小化老年代回收的停顿时间。老年代对象存活率高,单次 Full GC 可能停顿数秒,对响应敏感的服务是灾难。
  • 算法标记-清除,并把最耗时的"标记""清除"过程与用户线程并发执行,把 STW 切成两次极短停顿。

4.2 工作流程(一次 Major GC,含四个阶段)

CMS GC 流程

① 初始标记(STW)

  • 仅标记与 GC Roots 直接关联的老年代对象,速度极快。
  • 类比:点名时只点班长的名,让班长去清点小组成员。

② 并发标记(并发)

  • 与应用线程并发,从初始标记结果出发深度遍历老年代,标记所有可达对象。
  • 这一阶段最耗时,但因为是并发的,应用基本无感知。
  • 问题:并发过程中应用线程可能修改引用,导致漏标/多标。

③ 重新标记(STW)

  • 修正并发标记阶段的引用变动。
  • 通过 增量更新原始快照(SATB) 技术,仅重扫被修改过的引用,停顿比初始标记长,但远短于并发标记。

④ 并发清除(并发)

  • 与应用线程并发地清理死亡对象。

  • 关键问题:CMS 是标记-清除算法,不移动存活对象,会产生内存碎片。

    碎片严重到无法分配大对象时,会退化为一次 Serial Old 的 Full GC,导致长时间停顿。

4.3 特点与适用场景

优点:低延迟,化整为零的并发设计带来平滑响应。

致命缺点

  1. 内存碎片:可能提前触发 Full GC。
  2. 对 CPU 敏感:并发阶段占用线程资源,吞吐量下降。
  3. 浮动垃圾:并发清理时新产生的垃圾只能留到下次 GC,需预留空间,容易触发"并发失败"。

场景:响应时间敏感的 B/S 系统(Web 服务器、Java 桌面应用),愿意以牺牲吞吐量和预留内存换取更平滑的响应。

注:CMS 因碎片和复杂度问题,JDK 9 起废弃,JDK 14 移除。继任者是 G1,以及更新的 ZGCShenandoah


新一代收集器:G1(Garbage-First)

G1(Garbage-First) 是 CMS 的继任者,从 JDK 9 起成为服务端默认收集器。目标是面向服务端、可预测停顿时间、兼顾吞吐与低延迟的全功能收集器

5.1 核心设计思想

G1 抛弃了传统"连续物理分代"的堆布局,采用分区模型(Region)

Region 分区化堆内存

  • 整个堆被划分为多个大小固定(1MB~32MB,2 的幂)的 Region
  • 每个 Region 在逻辑上扮演 Eden / Survivor / Old / Humongous 之一,但物理位置不再连续。新生代和老年代变成一组 Region 的动态集合
Region 类型作用
Eden新生代
Survivor新生代
Old老年代
Humongous大对象

停顿时间模型

  • 通过 -XX:MaxGCPauseMillis 设定期望的最大停顿时间(如 200ms)。
  • G1 不再机械地回收整个新生代或老年代,而是优先回收价值最高(垃圾最多)的 Region——这就是名字 "Garbage-First" 的由来。

5.2 工作流程

阶段一:年轻代收集(STW,并行)

只在若干 Eden / Survivor Region 中进行:

  1. 根扫描:暂停应用线程,扫描 GC Roots。
  2. 处理 RSet:通过记忆集高效识别跨 Region 引用。
  3. 对象标记与复制:将存活对象复制到新的 Survivor Region 或晋升到 Old Region。
  4. 清空:回收完的 Region 加入空闲列表。

收集完后,G1 会评估每个 Old Region 的回收价值,为后续的混合收集做准备。

阶段二:并发标记周期

为混合收集提供全堆存活信息:

  1. 初始标记(STW) :标记 GC Roots 直接可达对象,通常搭车在一次年轻代收集中完成,开销极小。
  2. 根区域扫描:扫描 Old Region 到新生代的引用,必须赶在下一次年轻代收集之前完成。
  3. 并发标记(并发) :与用户线程并发,从 GC Roots 出发进行全堆可达性分析,耗时最长但应用无感知。
  4. 最终标记(STW) :处理并发标记阶段遗留下来的 SATB(原始快照)记录,完成标记。
  5. 清理(STW)计算每个 Region 的回收价值并排序,为混合收集做 Region 选择;完全清空无存活对象的 Region。

阶段三:混合收集

这是 G1 实现"在限定时间内回收最多垃圾"的核心阶段:

  • 并发标记周期后,G1 知道哪些 Old Region 的垃圾比例最高。
  • 混合收集 同时回收所有新生代 Region + 部分回收价值最高的老年代 Region。
  • 这是增量式的:不会一次性回收所有老年代 Region,而是分多次混合收集,每次只回收一部分(默认最多 8 个),将每次停顿控制在目标时间范围内。

(可能的)阶段四:Full GC

G1 尽力避免 Full GC,但以下情况会退化为单线程 Serial Old 的 Full GC:

  1. 并发模式失败:回收速度跟不上分配速度,老年代被填满。
  2. 晋升失败:混合收集时 Survivor 或老年代空间不足。
  3. 巨型对象分配失败

5.3 三大关键技术

  1. 记忆集(RSet)
    每个 Region 维护一个记忆集,记录其他 Region 对本 Region 的引用。解决了跨 Region 引用导致的全堆扫描问题,使 Region 可作为独立回收单元。
  2. SATB(原始快照) 并发标记开始时为堆创建逻辑"快照",之后新创建的引用不会被标记,但被删除的引用会被记录并重新扫描,保证不会漏标。
  3. 停顿预测模型
    基于每个 Region 的回收历史(卡表数量、复制时间等),建立可靠的停顿预测模型,根据目标停顿时间选择一组回收效益最高的 Region。

5.4 G1 vs. CMS

特性CMSG1
堆布局物理连续分代(新生代/老年代)分区模型(Region),逻辑分代
目标最小化老年代收集停顿可预测的停顿时间,兼顾吞吐与低延迟
碎片处理标记-清除,会产生内存碎片标记-复制(Region 间),天然整理,避免碎片
回收范围老年代(需与 ParNew 配合)整个堆(选择部分 Region 进行混合收集)
停顿控制被动(通过并发减少停顿)主动(基于模型的停顿时间预测与控制)

G1是一款划时代的收集器,它通过分区模型可预测的停顿时间模型,在相当程度上解决了CMS的内存碎片和不可控Full GC问题。它将垃圾收集的焦点从“分代”转向了“分区”,并为后续更先进的ZGC和Shenandoah奠定了思想和工程基础。

对于大内存(6GB以上)、对停顿时间敏感的应用,G1是一个非常稳健的默认选择。其核心优势在于:在保证相对高吞吐量的同时,提供了可预测的、相对较短的停顿时间


总结

本文从两个维度梳理了 JVM GC:

  1. GC 基本机制:Minor GC 负责年轻代(频繁、短停顿),Full GC 负责老年代(低频、长停顿),两者都会引发 STW。GC 通过追踪对象之间的出站/入站引用关系来判断可达性,需要注意循环引用和内存泄漏。
  2. 具体收集器
    • ParNew + CMS 是经典低延迟组合:ParNew 用复制算法快速回收新生代,CMS 用并发标记-清除在老年代实现低停顿,但存在内存碎片等致命缺陷(JDK 14 已移除)。
    • G1 是当前服务端默认收集器,通过 Region 分区模型和停顿时间模型,实现了可预测的停顿时间,在保证相对高吞吐的前提下,是大内存(6GB+)、对停顿敏感场景的稳健选择。

对于更极致的低延迟需求,还可以关注 ZGCShenandoah,它们以亚毫秒级停顿为目标,代表了 GC 的未来方向。

用心记录,持续成长